/* 
 * Copyright (C) 2014 Krzysztof Troska <elleander86 at gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package anidbjclient;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Menages configuration all configurable infos.
 *
 * @author Krzysztof Troska <elleander86 at gmail.com>
 */
public class ConfigManager {

    /**
     * Map storing all config values
     */
    public final Map<String, String> CONFIG = new HashMap<>();
    /**
     * Map storing all hardconded config values
     */
    public final Map<String, String> HARD_CONFIG = new HashMap<>();
    /**
     * List of options used to make key values
     */
    private static final List<String> options = Arrays.asList("Server", "Port",
            "User", "Password", "LocalPort", "Download_Folder", "Anime_Folder",
            "Extensions");
    /**
     * Path to a config file.
     */
    private static final Path PATH = Paths.get(new File("etc/main.conf").getAbsolutePath());

    /**
     * Puts default options into maps
     */
    ConfigManager() {
        // load default options name so we can read them from config file.
        options.stream().forEach((option) -> {
            CONFIG.put(option, null);
        });
        this.loadConfigFromFile();
        this.loadHardcodedMap();
    }

    /**
     * Appends hardcoded options into default map, if for some reason those
     * options was in the map we skill this.
     */
    private void loadHardcodedMap() {
        HARD_CONFIG.put("FMask", "78000008D0");
        HARD_CONFIG.put("AFileMask", "00000080");
        HARD_CONFIG.put("AAnimeMask", "F0809A01000000");
        HARD_CONFIG.put("Version", "1");
        HARD_CONFIG.put("BufSize", "1400");
        HARD_CONFIG.put("AnidbApiVersion", "3");
    }

    /**
     * Returns option by given key (option name). Remember that option is not
     * checked in any way.
     *
     * @param key - name of a option
     * @return option for given key or null
     */
    public String getOption(String key) {
        if (CONFIG.containsKey(key)) {
            return CONFIG.get(key);
        } else if (HARD_CONFIG.containsKey(key)) {
            return HARD_CONFIG.get(key);
        } else {
            return null; // should happend only 
        }
    }
    
    /**
     * Returns option by given key (option name). Remember that option is not
     * checked in any way.
     *
     * @param key - name of a option
     * @return option for given key or null
     */
    public int getOptionInt(String key) throws NumberFormatException {
        if (CONFIG.containsKey(key)) {
            return Integer.parseInt(CONFIG.get(key));
        } else if (HARD_CONFIG.containsKey(key)) {
            return Integer.parseInt(HARD_CONFIG.get(key));
        } else {
            return 0;
        }
    }

    /**
     * Get local port number from config file if local port is not between
     * 1025-65535 we return default
     *
     * @return local port or default 22356
     */
    public int getLocalPort() {
        try {
            int port = Integer.parseInt(getOption("LocalPort"));
            if(port > 1024 && port < 65535)
                return port;
        } catch (NumberFormatException e) {
            System.err.println("Wrong port number using default.");
        }
        return 22356;
    }
    
    /**
     * Checks if given extension is in our list
     * @param extension
     * @return true if list contains extension
     */
    public boolean compereExtensions(String extension) {
        List<String> extensions = Arrays.asList(getOption("Extensions").split("\\|"));
        return extensions.contains(extension);
    }

    /**
     * Sets local port we just check if port is higher than 1024 and lower than
     * 65535
     *
     * @param port
     * @return
     */
    public boolean setLocalPort(int port) {
        if (port < 1024 || port > 65535) {
            System.err.println("Local port need to be number bettwen 1025 and "
                    + "65535, no change made");
            return false;
        } else {
            CONFIG.put("LocalPort", Integer.toString(port));
            return true;
        }
    }

    /**
     * Returns download Path
     *
     * @return
     */
    public Path getDownloadPath() {
        try {
            return Paths.get(getOption("Download_Folder"));
        } catch (InvalidPathException e) {
            System.err.println("Invalid Download path using default.");
        }
        return Paths.get(System.getProperty("user.home"), "Downloads");
    }

    /**
     * Sets download directory we just check if it's realy directory
     *
     * @param path - path to a dir
     * @return ture if success
     */
    public boolean setDownloadPath(Path path) {
        if (path.toFile().isDirectory()) {
            CONFIG.put("Download_Folder", path.toString());
            return true;
        } else {
            System.err.println("Path is not a directory.");
            return false;
        }
    }

    /**
     * Returns anime path
     *
     * @return
     */
    public Path getAnimePath() {
        try {
            return Paths.get(getOption("Anime_Folder"));
        } catch (InvalidPathException e) {
            System.err.println("Invalid Anime path using default.");
        }
        return Paths.get(System.getProperty("user.home"), "Videos", "Anime");
    }

    /**
     * Sets anime path, we just check if it's directory
     *
     * @param path - path to a directory
     * @return ture if success
     */
    public boolean setAnimePath(Path path) {
        if (path.toFile().isDirectory()) {
            CONFIG.put("Anime_Folder", path.toString());
            return true;
        } else {
            System.err.println("Path is not a directory.");
            return false;
        }
    }

    /**
     * Returns anidb connection port
     *
     * @return
     */
    public int getPort() {
        try {
            int port = Integer.parseInt(getOption("Port"));
            if(port > 0 && port < 65535)
                return port;
        } catch (NumberFormatException e) {
            System.err.println("Wrong port number using default.");
        }
        return 9000;
    }

    /**
     * Sets anidb port, won't change it if the port is higher than 65535 or
     * lower than 1
     *
     * @param port
     * @return ture if success
     */
    public boolean setPort(int port) {
        if (port < 1 || port > 65535) {
            System.err.println("Anidb port need to be number bettwen 1 and "
                    + "65535, no change made");
            return false;
        } else {
            CONFIG.put("Port", Integer.toString(port));
            return true;
        }
    }

    /**
     * Gets user name or asks for it if not exists
     *
     * @return
     */
    public String getUserName() {
        return getOption("User");
    }

    /**
     * Sets storred user name
     *
     * @param name string of a name
     */
    public void setUserName(String name) {
        if (!name.isEmpty())
            CONFIG.put("User", name);
        else
            System.err.println("User name can't be empty.");
    }

    /**
     * Gets password or asks for it if not exists
     *
     * @return
     */
    public String getPassword() {
        return getOption("Password");
    }

    /**
     * Sets the password value for security resons array is then filled with
     * zeros (but still the password is stored inside txt file so there is no
     * security).
     *
     * @param password password in char array
     */
    public void setPassword(char[] password) {
        if (password.length >0) {
            CONFIG.put("Password", new String(password));
            Arrays.fill(password, '0');
        } else
            System.err.println("Password can't be empty.");
    }

    /**
     * Get inet Address of anidb server
     *
     * @return inetaddress of anidb or null when dns error
     */
    public InetAddress getServer() {
        try {
            return InetAddress.getByName(getOption("Server"));
        } catch (UnknownHostException e) {
            System.err.println("Cant resolve anidb server adres trying to use default one.");
            try {
                return InetAddress.getByName("api.anidb.net");
            } catch (UnknownHostException e1) {
                System.err.println("Cant resolve default anidb server name check internet connection.");
                return null;
            }
        }
    }

    /**
     * Get server name as a string for future change
     *
     * @return - Server name
     */
    public String getServerName() {
        return getOption("Server");
    }

    /**
     * Create default config file, if it exists saves backup adding ~ to the
     * beggining of name.
     *
     * @throws IOException If can't read from file or file don't exist.
     */
    private void createConfigFile() throws IOException {
        File file = PATH.toFile();
        File dir = PATH.getParent().toFile();
        if (file.exists()) {
            Files.move(PATH, Paths.get(PATH.getParent().toString(), "~" + PATH.toString()), StandardCopyOption.REPLACE_EXISTING);
            file.delete();
        }
        if (!dir.isDirectory() || !dir.mkdirs()) {
            throw new IOException("Cant create config file etc/main.conf.");
        }
        if (!file.createNewFile()) {
            throw new IOException("Cant create config file etc/main.conf.");
        }
    }

    /**
     * Saves actualy stored config into a file.
     */
    public synchronized void saveConfigToFile() {
        try {
            this.createConfigFile();
        } catch (IOException e) {
            System.err.println("Cant create config file etc/main.conf.");
            System.exit(0);
        }
        /* Now save info in file */
        try (PrintWriter out = new PrintWriter(PATH.toFile())) {
            out.println("##Config File for anidbJclient you want to change user,"
                    + " password, Download and Anime folder.");
            out.println("##Any of the lines in config can contain only one = "
                    + "(you can't use = in user_name or password).");
            CONFIG.entrySet().stream().forEach((entry) -> {
                out.println(entry.getKey() + "=" + entry.getValue());
            });
        } catch (IOException e) {
            System.err.println("Cant create config file etc/main.conf."); 
        }
    }

    /**
     * Reads line of config file into CONFIG map, drops line when a comment or
     * has more than one =
     *
     * @param line
     */
    private void readLine(String line) {
        if (line.matches("^##.*")) {
            return; //comment
        } else if (!line.matches("^[a-zA-z_]+=[^=]+$")) {
            System.err.format("Line: %s\n Is not vaild configuration file line.\n", line);
            return;
        }
        /**
         * Small waste of memory but it makes code more redable
         */
        String tmp[] = line.split("=");
        String key = tmp[0];
        String value = tmp[1];
        /**
         * Check if this key value is in options, if not we drop it silently
         */
        if (options.contains(key)) {
            CONFIG.put(key, value); // put new or update
        }
    }

    /**
     * Loads config from file
     */
    private synchronized void loadConfigFromFile() {
        /**
         * Check if config file exist if not we try to create default one TODO:
         * make it throw IOException and call some option creation app
         */
        List<String> lines;
        try {
            lines = Files.readAllLines(PATH, Charset.defaultCharset());
        } catch (NoSuchFileException e) {
            System.err.println("Config File don't exist loading default values.");
            this.loadDefaultConfig();
            this.saveConfigToFile();
            return;
        } catch (IOException ex) {
            System.err.println("Can't read from confing file.");
            return;
        }
        lines.stream().forEach((line) -> {
            this.readLine(line);
        });
    }
    
    /**
     * Overwrites config map with default values.
     */
    private void loadDefaultConfig() {
        Map<String, String> map = new HashMap<>();
        map.put("Server", "api.anidb.net");
        map.put("Port", "9000");
        map.put("LocalPort", "22356");
        map.put("Download_Folder", Paths.get(System.getProperty("user.home"), "Downloads").toString());
        map.put("Anime_Folder", Paths.get(System.getProperty("user.home"), "Videos", "Anime").toString());
        map.put("Extensions", "!qB|!ut");
        map.put("User", "user_name");
        map.put("Password", "password");
        CONFIG.putAll(map);
    }
}
